﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Djs.Tools.Tresor.Data
{
    /// <summary>
    /// TresorText : slouží ke konverzi dat na text a naopak, na základě mapy znaků
    /// </summary>
    internal class TresorText
    {
        #region Konstrukce
        internal TresorText(char[] charMap)
        {
            int length = (charMap == null ? -1 : charMap.Length);
            if (length != 16)
                throw new ArgumentException("TresorText musí být inicializován mapou 16-ti znaků.");

            this.CharMap = new char[charMap.Length];
            this.CharDict = new Dictionary<char, byte>();
            for (int i = 0; i < this.CharMap.Length; i++)
            {
                char c = charMap[i];
                this.CharMap[i] = c;
                this.CharDict.Add(c, (byte)i);
            }

            this.FreeVocals = FindFreeChars(ALL_VOCALS);
            this.FreeConsonants = FindFreeChars(ALL_CONSONANTS);
            this.FreeSemiConsonants = FindFreeChars(ALL_SEMI_CONSONANTS);
            this.FreeNumerics = FindFreeChars(ALL_NUMERICS);
            this.FreeNonChars = FindFreeChars(ALL_NON_CHARS);

            this.Rand = new Random();
        }

        private string FindFreeChars(string text)
        {
            string free = "";
            foreach (char c in text)
            {
                if (!this.CharDict.ContainsKey(c))
                    free += c;
            }
            return free;
        }
        private char[] CharMap;

        private string FreeVocals;
        private string FreeConsonants;
        private string FreeSemiConsonants;
        private string FreeNumerics;
        private string FreeNonChars;
        internal const string ALL_CODES = "bcdfghjklmnpqrstvwz"; // Abeceda: abcdefghijklmnopqrstuvwxyz = 26 znaků.  Vyloučíme  aeiouxy = 7 znaků, zbývá 19 znaků, z nich se vybere 16.
        internal const string ALL_VOCALS = "aeiouy";
        internal const string ALL_CONSONANTS = "bcdfghjklmnpqrstvwxz";
        internal const string ALL_SEMI_CONSONANTS = "lrnm";
        internal const string ALL_NUMERICS = "0123456789";
        internal const string ALL_NON_CHARS = ",;.:!?";
        private Dictionary<char, byte> CharDict;
        private Random Rand;
        /// <summary>
        /// Vrací true v daném počtu procenta pravděpodobnosti.
        /// Tedy: pokud se zadá 20, pak ve 20 případech ze 100 vrátí true, v 80 případech ze 100 vrací false.
        /// Volání IsRandom(procento) vyjadřuje tedy: "S pravděpodobností větší než procento se provede toto:"
        /// </summary>
        /// <param name="percent"></param>
        /// <returns></returns>
        private bool IsRandom(int percent)
        {
            return (this.Rand.Next(0, 100) < percent);
        }
        #endregion
        #region Crypt
        internal void Crypt(System.IO.Stream input, System.IO.TextWriter output)
        {
            CryptContext context = new CryptContext();
            while (true)
            {
                int value = input.ReadByte();
                if (value < 0) break;

                int lo = value & 0x000F;
                CryptValue(lo, context, output);

                int hi = (value & 0x00F0) >> 4;
                CryptValue(hi, context, output);
            }

            if (context.NeedEnd)
            {
                if (!context.StartNewSentence)
                {
                    context.Buffer += ".";
                    context.StartNewSentence = true;
                }
                CryptWrite(context, output);
            }

            output.Flush();
        }
        private void CryptValue(int value, CryptContext context, System.IO.TextWriter output)
        {
            char c = this.CharMap[value];
            CryptChar(c, context, output);
        }
        private void CryptChar(char c, CryptContext context, System.IO.TextWriter output)
        {
            if (context.Buffer.Length > 0)
            {
                CharType innerType = GetRandomInnerCharType(context.Buffer[context.Buffer.Length - 1], c);
                if (innerType != CharType.None)
                    context.Buffer += GetRandomCharOfType(innerType);
            }
            if (context.StartNewSentence)
            {
                c = Char.ToUpper(c);
                context.StartNewSentence = false;
                context.SentenceWordCount = 0;
            }
            context.Buffer += c;
            if (context.Buffer.Length > this.Rand.Next(2, 10))
            {   // Ukončíme slovo:

                // Odsadíme stávající řádek:
                if ((context.OneLineLength > 20 && IsRandom(15)) ||
                    (context.OneLineLength > 65 && IsRandom(45)) ||
                    (context.OneLineLength > 85 && IsRandom(80)) ||
                    (context.OneLineLength > 120 && IsRandom(90)) ||
                    (context.OneLineLength > 175))
                {
                    context.Buffer = "\r\n" + context.Buffer;
                    context.OneLineLength = 0;
                }

                if ((context.SentenceWordCount > 1 && IsRandom(15)) ||
                    (context.SentenceWordCount > 5 && IsRandom(65)) ||
                    (context.SentenceWordCount > 8 && IsRandom(85)) ||
                    (context.SentenceWordCount > 12))
                {
                    context.Buffer += ".";
                    context.StartNewSentence = true;
                    context.SentenceWordCount = 0;
                }
                else if (IsRandom(20))
                {
                    context.Buffer += ",";
                    context.NeedWord = true;
                }
                context.Buffer += " ";
                context.SentenceWordCount++;

                this.CryptWrite(context, output);
            }
        }
        private void CryptWrite(CryptContext context, System.IO.TextWriter output)
        {
            int length = context.Buffer.Length;
            foreach (char c in context.Buffer)
                output.Write(c);
            context.SentenceLength += length;
            context.OneLineLength += length;
            context.Buffer = "";
        }
        private CharType GetCharType(char c)
        {
            if (ALL_VOCALS.Contains(c)) return CharType.Vocal;
            if (ALL_CONSONANTS.Contains(c)) return CharType.Consonant;
            if (ALL_NUMERICS.Contains(c)) return CharType.Numeric;
            if (ALL_NON_CHARS.Contains(c)) return CharType.Other;
            return CharType.None;
        }
        private string GetRandomCharOfType(CharType type)
        {
            string chars = (type == CharType.Vocal ? this.FreeVocals :
                           (type == CharType.Consonant ? this.FreeConsonants :
                           (type == CharType.Numeric ? this.FreeNumerics :
                           (type == CharType.Other ? this.FreeNonChars : ""))));
            int length = chars.Length;
            if (length == 0) return "";
            return chars[this.Rand.Next(0, length)].ToString();
        }
        private CharType GetRandomInnerCharType(char c1, char c2)
        {
            CharType ct1 = GetCharType(c1);
            CharType ct2 = GetCharType(c2);

            int rand = this.Rand.Next(0,100);
            if (ct1 == CharType.Consonant && ct2 == CharType.Consonant && IsRandom(80)) return CharType.Vocal;          // Mezi dvě souhlásky dám s pravděpodobností větší než 80% jednu samohlásku (HT => HAT)
            if (ct1 == CharType.Vocal && ct2 == CharType.Vocal && IsRandom(75)) return CharType.Consonant;              // Mezi dvě samohlásky dám s pravděpodobností větší než 75% jednu samohlásku (AI => ANI)
            if (ct1 == CharType.Consonant && ct2 == CharType.Vocal && IsRandom(10)) return CharType.Vocal;
            if (ct1 == CharType.Vocal && ct2 == CharType.Consonant && IsRandom(10)) return CharType.Vocal;

            return CharType.None;
        }
        private enum CharType
        {
            None, 
            /// <summary>AEIOUY</summary>
            Vocal,
            /// <summary>BCDFGHJKLMNPQRSTVWXZ</summary>
            Consonant,
            /// <summary>012345789</summary>
            Numeric,
            /// <summary>,;.:!?</summary>
            Other
        }
        private class CryptContext
        {
            public CryptContext()
            {
                this.Buffer = "";
                this.StartNewSentence = true;
                this.NeedWord = false;
                this.SentenceWordCount = 0;
                this.SentenceLength = 0;
                this.OneLineLength = 0;
            }
            public string Buffer;
            public bool StartNewSentence;
            public bool NeedWord;
            public int SentenceWordCount;
            public int SentenceLength;
            public int OneLineLength;
            public bool NeedEnd { get { return (this.Buffer.Length > 0 || this.NeedWord); } }
        }
        #endregion
        #region Decrypt
        /// <summary>
        /// 
        /// </summary>
        /// <param name="input"></param>
        /// <param name="mw"></param>
        internal void Decrypt(System.IO.TextReader input, System.IO.Stream output)
        {
            int accumulator = 0;
            int accumulatorWidth = 0; 
            while (true)
            {
                int code = input.Read();                // znak a-z
                if (code < 0) break;

                char c = (char)code;
                c = Char.ToLower(c);
                byte v;
                if (this.CharDict.TryGetValue(c, out v))
                {
                    int value = v & 0x0F;
                    if (accumulatorWidth > 0)
                        value = value << accumulatorWidth;
                    accumulator |= value;
                    accumulatorWidth += 4;

                    if (accumulatorWidth >= 8)
                    {
                        output.WriteByte((byte)(accumulator & 0x00FF));
                        accumulatorWidth -= 8;
                        accumulator = accumulator >> 8;
                    }
                }
            }
            if (accumulatorWidth >= 8)
            {
                output.WriteByte((byte)(accumulator & 0x00FF));
                accumulatorWidth -= 8;
                accumulator = accumulator >> 8;
            }
        }
        #endregion
    }
}
